/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package de.saar.penguin.give3dreplay;

import com.jme.app.SimpleGame;
import com.jme.input.InputSystem;
import com.jme.input.KeyInput;
import com.jme.input.MouseInput;
import com.jme.light.LightNode;
import com.jme.light.PointLight;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.*;
import com.jme.scene.Spatial.CullHint;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.shape.Box;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.system.GameSettings;
import com.jme.system.JmeException;
import com.jme.system.PreferencesGameSettings;
import de.saar.penguin.give3dreplay.eyetracking.FixationInterpreter;
import de.saar.penguin.give3dreplay.eyetracking.FixationMarker;
import give.client.client3d.Client3DGame;
import give.client.client3d.JmeUtil;
import give.client.client3d.sceneobject.*;
import give.client.swing.JmeInitProgressFrame;
import give.comm.*;
import give.comm.handling.MessageHandlingException;
import give.formula.Atom;
import give.formula.Formula;
import give.formula.Orientation;
import give.formula.Position;
import give.replay.ReplayWindow;
import give.world.World;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.prefs.Preferences;

/**
 *
 * @author koller
 */
public class ReplayWindow3D extends SimpleGame implements ReplayWindow {
    private static final float AVATAR_HEIGHT = 150;            // height of camera above ground
    private static final float HALF_CAMERA_ANGLE = 53 * FastMath.DEG_TO_RAD; // 106 deg view angle
    private static final String CONFIGURATION_FILENAME = "client-configuration.xml";
    private static final float TILE_SIZE = 150;
    private static final int MINXPOS = 0;
    private static final int MAXXPOS = 15;
    private static final int MINZPOS = 0;
    private static final int MAXZPOS = 14;
    private final Queue<Message> messageQueue;
    private final Queue<List<Formula>> worldChangeQueue;
    private JmeInitProgressFrame progressFrame;
    private LightNode lightNode;
    private World world;
    private String windowTitle;
    private CameraNode cn;
    private static final Vector3f left = new Vector3f(-1.0f, 0.0f, 0.0f);
    private static final Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
    private Map<String, SceneObject> objectsByName;
    private FixationMarker fmarker;
    private FixationInterpreter fixationInterpreter;
//    private int clientWindowHeight, clientWindowWidth;
    protected Node fpsNode;
    protected Text fps;
    private TextDisplay textdisplay;
//    private LightState lightState;
    public static String fontLocation = Text.DEFAULT_FONT;
    private boolean gameEnded;
    private boolean textdisplayEmitBeeps = false;
    private int textdisplayDisplayLifetime = TextDisplay.DEFAULT_DISPLAY_LIFETIME;
    private GameThread gameThread;
    private String eyetrackingEventSourceName;
    private boolean isReady;
    private int replayWindowHeight, replayWindowWidth;
    private double windowScalingX, windowScalingY;
    private boolean ignoreTextMessages;
    private String overridingWindowTitle;

    public ReplayWindow3D(int width, int height) {
        isReady = false;
        messageQueue = new ArrayBlockingQueue<Message>(10000, true);
        worldChangeQueue = new ArrayBlockingQueue<List<Formula>>(10000, true);
        objectsByName = new HashMap<String, SceneObject>();

        replayWindowWidth = width;
        replayWindowHeight = height;
        windowScalingX = 1;
        windowScalingY = 1;

        ignoreTextMessages = false;
        overridingWindowTitle = null;

        setConfigShowMode(ConfigShowMode.NeverShow);
        fixationInterpreter = new FixationInterpreter(objectsByName);
        eyetrackingEventSourceName = null;

        MouseInput.setProvider(InputSystem.INPUT_SYSTEM_AWT);
        KeyInput.setProvider(InputSystem.INPUT_SYSTEM_AWT);
    }

    public void setClientDimensions(int width, int height) {
        windowScalingX = ((double) replayWindowWidth) / width;
        windowScalingY = ((double) replayWindowHeight) / height;
        System.err.println("Client window dimension is " + width + "x" + height);
    }

    public void setEyetrackingEventSourceName(String name) {
        eyetrackingEventSourceName = name;
    }
    
    /**
     * Instructs the replay window to never display text messages.
     * (Useful for creating replays of games that had synthesized speech output.)
     * 
     * @param val 
     */
    public void setIgnoreTextMessages(boolean val) {
        ignoreTextMessages = val;
    }
    
    /**
     * Sets an overriding window title. This title will be used for all
     * replay windows, even if a different title is specified in the call
     * to openWindow.
     * 
     * @param val 
     */
    public void setOverridingWindowTitle(String val) {
        overridingWindowTitle = val;
    }

    public List<String> getFixatedObjects(int x, int y) {
        return fixationInterpreter.getFixatedObjects(convertX(x), convertY(y));
    }

    @Override
    protected GameSettings getNewSettings() {
        Preferences prefs = Preferences.userRoot();
        prefs.put("GameRenderer", "LWJGL");
        prefs.putInt("GameFrequency", 60);
        prefs.putInt("GameWidth", replayWindowWidth);
        prefs.putInt("GameHeight", replayWindowHeight);
        prefs.putInt("GameDepth", 32);
        prefs.putBoolean("GameFullscreen", false);

        return new PreferencesGameSettings(prefs);
    }

    @Override
    protected void simpleInitGame() {
        progressFrame = new JmeInitProgressFrame(world.getUniverse().size());
        progressFrame.setVisible(true);

        try {
            ObjectLoader.load(Client3DGame.class.getClassLoader().getResourceAsStream(CONFIGURATION_FILENAME));
        } catch (Exception e) {
            System.err.println("Couldn't load the configuration file.");
            e.printStackTrace(System.err);
            System.exit(1);
        }

        // Setup blend state
        BlendState as1 = display.getRenderer().createBlendState();
        as1.setBlendEnabled(true);
        as1.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
        as1.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
        as1.setTestEnabled(false);
        as1.setEnabled(true);
        fpsNode = new Node("FPS node");
        fpsNode.setRenderState(as1);
        fpsNode.setCullHint(Spatial.CullHint.Never);

        // add a light source
        buildLighting();

        // add the world objects
        progressFrame.update(1);
        addWorldObjects();

        // make floor and ceiling
        buildTerrain();

        progressFrame.update(world.getUniverse().size() + 2);
        progressFrame.setVisible(false);

        // set up text display
        textdisplay = new TextDisplay(fpsNode);
        textdisplay.setDisplayLifetime(textdisplayDisplayLifetime);
        textdisplay.setEmitBeep(textdisplayEmitBeeps);
        rootNode.attachChild(fpsNode);

        // position camera
        synchronized (world) {
            Vector3f initialCameraLocation = JmeUtil.getVector(getJmePosition(world.getPlayerPosition()));
            initialCameraLocation.y = AVATAR_HEIGHT;
            Vector3f initialCameraOrientation = JmeUtil.getVector(world.getPlayerOrientation());

            cam.setFrame(initialCameraLocation, left, up, initialCameraOrientation);

            // ... and attach it to a camera node
            cn = new CameraNode("camnode", cam);
            rootNode.attachChild(cn);

            cn.setLocalTranslation(new Vector3f(initialCameraLocation.x, AVATAR_HEIGHT, initialCameraLocation.z));


            float angle = JmeUtil.getAngle(initialCameraOrientation, new Vector3f(0, 0, 1));
            Quaternion rot = new Quaternion();
            rot.fromAngleAxis(angle, up);
            cn.setLocalRotation(rot);
        }

        fmarker = new FixationMarker(statNode, (float) (7 * windowScalingX), 10);
    }

    public boolean isReady() {
        return isReady;
    }

    @Override
    protected void simpleUpdate() {
        boolean cameraMoved = false;
        boolean statesChanged = false;

        isReady = true;

        if (gameEnded) {
            removeWorldObjects();
            messageQueue.clear();
            worldChangeQueue.clear();
            reinitGame();
            cameraMoved = true;
            statesChanged = true;
            display.setTitle(windowTitle);
        }


        synchronized (messageQueue) {
            long startTime = System.currentTimeMillis();
            int messages = 0;

            while (!messageQueue.isEmpty()) {
                Message msg = messageQueue.remove();
                messages++;

                if (msg instanceof StatusMessage) {
                    StatusMessage sm = (StatusMessage) msg;

                    Vector3f cameraLocation = JmeUtil.getVector(getJmePosition(sm.getPosition()));
                    cameraLocation.y = AVATAR_HEIGHT;
                    Vector3f cameraOrientation = JmeUtil.getVector(sm.getOrientation());

                    // move camera node to appropriate position and orientation
                    cn.setLocalTranslation(cameraLocation);

                    float angle = JmeUtil.getAngle(cameraOrientation, new Vector3f(0, 0, 1));
                    Quaternion rot = new Quaternion();
                    rot.fromAngleAxis(angle, up);
                    cn.setLocalRotation(rot);

                    cameraMoved = true;

                    // remember visible objects
                    fixationInterpreter.setVisibleObjects(sm.getVisibleObjects());
                } else if (msg instanceof ActionMessage) {
                    while (!worldChangeQueue.isEmpty()) {
                        List<Formula> changes = worldChangeQueue.remove();

                        for (Formula change : changes) {
                            if (change instanceof Atom) {
                                Atom atom = (Atom) change;
                                SceneObject obj = objectsByName.get(atom.getArgumentAsString(0));

                                if (obj == null) {
                                    System.err.println("** ReplayWindow3D: couldn't identify object of world change " + atom);
                                }

                                if (atom.getPredicate().equals("state")) {
                                    obj.changeState(atom.getArgumentAsString(1));
//                                    System.err.println("  -> changed state of " + atom.getArgumentAsString(0) + " to " + atom.getArgumentAsString(1));
                                    statesChanged = true;
                                } else if (atom.getPredicate().equals("position")) {
                                    Position newpos = (((Position) atom.getArguments().get(1)));
                                    obj.changePosition(newpos);
                                } else if (atom.getPredicate().equals("orientation")) {
                                    Orientation newori = (((Orientation) atom.getArguments().get(1)));
                                    obj.changeOrientation(newori);
                                } else if (atom.getPredicate().equals("holding")) {
                                    obj.hide();
                                }
                            }
                        }
                    }
                } else if (msg instanceof TextMessage) {
                    displayText(((TextMessage) msg).getText());
                } else if (msg instanceof EventSourceMessage) {
                    EventSourceMessage esm = (EventSourceMessage) msg;

                    if (esm.getEventSource().equals(eyetrackingEventSourceName)) {
                        // convert coordinates we received from client to the size of the replay window
                        int x = convertX(Integer.parseInt(esm.getData().get("x")));
                        int y = convertY(Integer.parseInt(esm.getData().get("y")));

                        // draw fixation marker
                        fmarker.fixate(x, y);

                        // compute fixated object(s)
                        List<String> fixatedObjects = fixationInterpreter.getFixatedObjects(x, y);
                        markFixatedObjects(fixatedObjects);
                    }
                }
            }

//            System.err.println("processed " + messages + " messages in " + (System.currentTimeMillis() - startTime) + " ms");
        }

        if (cameraMoved) {
            cn.updateModelBound();
            cn.updateWorldBound();

            lightNode.setLocalTranslation(cn.getLocalTranslation());
        }

        if (statesChanged) {
            rootNode.updateRenderState();
        }
    }

    // convert from client screen coordinates to replay window coordinates
    private int convertX(int clientX) {
        return (int) Math.floor(windowScalingX * clientX);
    }

    private int convertY(int clientY) {
        return replayWindowHeight - (int) Math.floor(windowScalingY * clientY);
    }

    @Override
    protected void initSystem() throws JmeException {
        super.initSystem();

        display.setTitle(windowTitle);

        float near = 1;
        float far = 10000;
        float width = FastMath.tan(HALF_CAMERA_ANGLE) * near;
        float aspect = (float) display.getHeight() / (float) display.getWidth();
        float height = width * aspect;
        cam.setFrustum(near, far, -width, width, height, -height);
        cam.setParallelProjection(false);
        cam.update();

    }

    private Position getJmePosition(Position pos) {
        return JmeUtil.getJmePosition(pos, TILE_SIZE);
    }

    private void buildLighting() {
        PointLight light = new PointLight();
        light.setDiffuse(ColorRGBA.white);
        light.setAmbient(ColorRGBA.white);
        light.setLocation(new Vector3f(1500, 200, 300));
        light.setEnabled(true);

        lightState = DisplaySystem.getDisplaySystem().getRenderer().createLightState();
        lightState.attach(light);
        lightState.setEnabled(true);

        lightNode = new LightNode("Light Node");
        lightNode.setLight(light);

        rootNode.attachChild(lightNode);
        rootNode.setRenderState(lightState);
    }

    private void buildTerrain() {
        TextureState ts = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
        ts.setEnabled(true);
        ts.setTexture(ObjectLoader.get().loadTexture("floor"));

        TextureState ts2 = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
        ts2.setEnabled(true);
        ts2.setTexture(ObjectLoader.get().loadTexture("ceiling"));

        for (int x = MINXPOS; x <= MAXXPOS; x++) {
            for (int z = MINZPOS; z <= MAXZPOS; z++) {
                Box tile = new Box("floor" + x + "," + z,
                        new Vector3f(TILE_SIZE * (x + 0.5f), -0.05f * TILE_SIZE, TILE_SIZE * (z + 0.5f)),
                        // -0.5f = lower end of walls
                        TILE_SIZE / 2, 10, TILE_SIZE / 2);
                tile.setRenderState(ts);
                rootNode.attachChild(tile);

                Box ceil = new Box("ceiling" + x + "," + z,
                        new Vector3f(TILE_SIZE * (x + 0.5f), 1.93f * TILE_SIZE, TILE_SIZE * (z + 0.5f)),
                        // 1.93f = upper end of walls
                        TILE_SIZE / 2, 10, TILE_SIZE / 2);
                ceil.setRenderState(ts2);
                rootNode.attachChild(ceil);
            }
        }
    }

    private void addWorldObjects() {
        int index = 1;

        synchronized (world) {

            try {
                for (String name : world.getUniverse()) {
                    if (world.getType(name).equals("button")) {
                        addWorldObject(name, new Button(name, world.getPosition(name), world.getOrientation(name), world, TILE_SIZE));
                    } else if (world.getType(name).equals("wall")) {
                        addWorldObject(name, new Wall(name, getPositiveRoomTexture(name, world), getNegativeRoomTexture(name, world), world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("door")) {
                        addWorldObject(name, new Door(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("picture")) {
                        addWorldObject(name, new Picture(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("stateless-button")) {
                        addWorldObject(name, new StatelessButton(name, world.getPosition(name), world.getOrientation(name), world, TILE_SIZE));
                    } else if (world.getType(name).equals("alarm")) {
                        addWorldObject(name, new AlarmedTile(name, world.getPosition(name), TILE_SIZE));
                    } else if (world.getType(name).equals("safe")) {
                        addWorldObject(name, new Safe(name, getNegativeRoomTexture(name, world), world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("trophy")) {
                        addWorldObject(name, new Trophy(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("chair")) {
                        addWorldObject(name, new Chair(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE, lightState));
                    } else if (world.getType(name).equals("couch")) {
                        addWorldObject(name, new Couch(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("table")) {
                        addWorldObject(name, new Table(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("bed")) {
                        addWorldObject(name, new Bed(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("windowwide")) {
                        addWorldObject(name, new Window(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("dresser")) {
                        addWorldObject(name, new Dresser(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("pedestal")) {
                        addWorldObject(name, new Pedestal(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("flower")) {
                        addWorldObject(name, new Flower(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE));
                    } else if (world.getType(name).equals("lamp")) {
                        addWorldObject(name, new Lamp(name, world.getPosition(name), world.getOrientation(name), TILE_SIZE, lightState));
                    }

                    index++;
                    progressFrame.update(index);
                }
            } catch (IOException e) {
                System.err.println(e);
            }
        }
    }

    private void addWorldObject(String name, SceneObject obj) {
        if (world.getState(name) != null) {
            obj.changeState(world.getState(name));
        }

        for (Spatial sp : obj.getSpatials()) {
            rootNode.attachChild(sp);
        }


        /*
         * rootNode.updateModelBound(); rootNode.updateWorldBound();
         *
         *
         * try { BoundingBox bbox = (BoundingBox)
         * obj.getSpatials().get(0).getWorldBound().clone(null); for (int i = 1;
         * i < obj.getSpatials().size(); i++) {
         * bbox.mergeLocal(obj.getSpatials().get(i).getWorldBound()); } Box b =
         * new Box("bbox for " + name, bbox.getCenter(), bbox.xExtent,
         * bbox.yExtent, bbox.zExtent); rootNode.attachChild(b); } catch
         * (Throwable e) { System.err.println("******* " + obj + " has no
         * bbox"); }
         *
         */

        objectsByName.put(name, obj);
    }

    private synchronized void removeWorldObjects() {
        SceneObject obj;
        for (String name : objectsByName.keySet()) {
            obj = objectsByName.get(name);
            for (Spatial sp : obj.getSpatials()) {
                rootNode.detachChild(sp);
            }
        }
    }

    protected void reinitGame() {
        progressFrame = new JmeInitProgressFrame(world.getUniverse().size());
        progressFrame.setVisible(true);

        try {
            ObjectLoader.load(Client3DGame.class.getClassLoader().getResourceAsStream(CONFIGURATION_FILENAME));
        } catch (Exception e) {
            System.err.println("Couldn't load the configuration file.");
            e.printStackTrace(System.err);
            System.exit(1);
        }

        progressFrame.update(1);
        addWorldObjects();

        progressFrame.update(world.getUniverse().size() + 2);

        // position camera
        Vector3f initialCameraLocation = JmeUtil.getVector(getJmePosition(world.getPlayerPosition()));
        initialCameraLocation.y = AVATAR_HEIGHT;
        Vector3f initialCameraOrientation = JmeUtil.getVector(world.getPlayerOrientation());

        cam.setFrame(initialCameraLocation, left, up, initialCameraOrientation);
        cn.setLocalTranslation(new Vector3f(initialCameraLocation.x, AVATAR_HEIGHT, initialCameraLocation.z));
        float angle = JmeUtil.getAngle(initialCameraOrientation, new Vector3f(0, 0, 1));
        Quaternion rot = new Quaternion();
        rot.fromAngleAxis(angle, up);
        cn.setLocalRotation(rot);

        progressFrame.setVisible(false);
        gameEnded = false;
    }

    public synchronized void endGame() {
        gameEnded = true;
    }

    public void displayText(String text) {
        if (!ignoreTextMessages) {
            textdisplay.display(text);
        }
    }

    private String getPositiveRoomTexture(String objectName, World world) {
        return world.getValueAsString(world.getValueAsString(objectName, "positive-room"), "color");
    }

    private String getNegativeRoomTexture(String objectName, World world) {
        return world.getValueAsString(world.getValueAsString(objectName, "negative-room"), "color");
    }

    public void handleException(MessageHandlingException mhe) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void handleStatusMessage(StatusMessage sm) throws MessageHandlingException {
        synchronized (messageQueue) {
            messageQueue.add(sm);
//            System.err.println("-- rw3d: queue size=" + messageQueue.size());
        }
    }

    public void handleActionMessage(ActionMessage am, List<Formula> list) throws MessageHandlingException {
        synchronized (messageQueue) {
            messageQueue.add(am);
//            System.err.println("-- rw3d: queue size=" + messageQueue.size());
            worldChangeQueue.add(list);
        }
    }

    public void handleMessage(Message msg) throws MessageHandlingException {
        synchronized (messageQueue) {
            messageQueue.add(msg);
//            System.err.println("-- rw3d: queue size=" + messageQueue.size());
        }
    }

    private void markFixatedObjects(List<String> fixatedObjects) {
        rootNode.detachChildNamed("fixatedObjectMarker");

        if (!fixatedObjects.isEmpty()) {
            String obj = fixatedObjects.get(0);
            Spatial sp = objectsByName.get(obj).getSpatials().get(0);
            Vector3f center = sp.getWorldBound().getCenter();
            Vector3f centerOnScreen = DisplaySystem.getDisplaySystem().getScreenCoordinates(center);

            Circle marker = new Circle("fixatedObjectMarker", 50, (float) (50.0 * windowScalingX));
            marker.setDefaultColor(ColorRGBA.yellow);

            marker.setRenderQueueMode(Renderer.QUEUE_ORTHO);
            marker.setLocalTranslation(centerOnScreen);

            marker.setLightCombineMode(LightCombineMode.Off);
            marker.setCullHint(CullHint.Never);

            marker.updateRenderState();

            rootNode.attachChild(marker);
            rootNode.updateGeometricState(0.0f, true);
            rootNode.updateRenderState();
        }

    }

    public void openReplayWindow(World world, String windowTitle, ClientMetadata clientMetadata) {
        this.world = world;

        if (overridingWindowTitle == null) {
            this.windowTitle = windowTitle;
        } else {
            this.windowTitle = overridingWindowTitle;
        }

        if (clientMetadata != null) {
            setClientDimensions(clientMetadata.getClientWindowWidth(), clientMetadata.getClientWindowHeight());
        }

        if (gameThread != null) {
            endGame();
        } else {
            gameThread = new GameThread(this);
            gameThread.start();
        }
    }

    public void synchronizeWorld() {
        synchronized (messageQueue) {
            messageQueue.clear();
            worldChangeQueue.clear();

            SceneObject obj;
            synchronized (world) {
                for (String name : objectsByName.keySet()) {
                    obj = objectsByName.get(name);
                    if (world.getState(name) != null) {
                        obj.changeState(world.getState(name));
                    }
                }
            }
        }
    }

    private class GameThread extends Thread {
        protected ReplayWindow3D game;

        public GameThread(ReplayWindow3D g) {
            super();
            this.game = g;
        }

        public void run() {
            game.start();
        }
    }
}
